/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package pt.up.beta.mobile.contacts;

import java.io.ByteArrayOutputStream;

import pt.up.beta.mobile.Constants;
import pt.up.beta.mobile.R;
import pt.up.beta.mobile.content.SigarraContract;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils;

/**
 * Helper class for storing data in the platform content providers.
 */
public class ContactOperations {
	private final ContentValues mValues;
	private final BatchOperation mBatchOperation;
	private final Context mContext;
	private boolean mIsSyncOperation;
	private long mRawContactId;
	private int mBackReference;
	private boolean mIsNewContact;

	/**
	 * Since we're sending a lot of contact provider operations in a single
	 * batched operation, we want to make sure that we "yield" periodically so
	 * that the Contact Provider can write changes to the DB, and can open a new
	 * transaction. This prevents ANR (application not responding) errors. The
	 * recommended time to specify that a yield is permitted is with the first
	 * operation on a particular contact. So if we're updating multiple fields
	 * for a single contact, we make sure that we call withYieldAllowed(true) on
	 * the first field that we update. We use mIsYieldAllowed to keep track of
	 * what value we should pass to withYieldAllowed().
	 */
	private boolean mIsYieldAllowed;

	/**
	 * Returns an instance of ContactOperations instance for adding new contact
	 * to the platform contacts provider.
	 * 
	 * @param context
	 *            the Authenticator Activity context
	 * @param userId
	 *            the userId of the sample SyncAdapter user object
	 * @param accountName
	 *            the username for the SyncAdapter account
	 * @param isSyncOperation
	 *            are we executing this as part of a sync operation?
	 * @return instance of ContactOperations
	 */
	public static ContactOperations createNewContact(Context context,
			String userId, String accountName, boolean isSyncOperation,
			BatchOperation batchOperation) {
		return new ContactOperations(context, userId, accountName,
				isSyncOperation, batchOperation);
	}

	/**
	 * Returns an instance of ContactOperations for updating existing contact in
	 * the platform contacts provider.
	 * 
	 * @param context
	 *            the Authenticator Activity context
	 * @param rawContactId
	 *            the unique Id of the existing rawContact
	 * @param isSyncOperation
	 *            are we executing this as part of a sync operation?
	 * @return instance of ContactOperations
	 */
	public static ContactOperations updateExistingContact(Context context,
			long rawContactId, boolean isSyncOperation,
			BatchOperation batchOperation) {
		return new ContactOperations(context, rawContactId, isSyncOperation,
				batchOperation);
	}

	public ContactOperations(Context context, boolean isSyncOperation,
			BatchOperation batchOperation) {
		mValues = new ContentValues();
		mIsYieldAllowed = true;
		mIsSyncOperation = isSyncOperation;
		mContext = context;
		mBatchOperation = batchOperation;
	}

	public ContactOperations(Context context, String userId,
			String accountName, boolean isSyncOperation,
			BatchOperation batchOperation) {
		this(context, isSyncOperation, batchOperation);
		mBackReference = mBatchOperation.size();
		mIsNewContact = true;
		mValues.put(RawContacts.SOURCE_ID, userId);
		mValues.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
		mValues.put(RawContacts.ACCOUNT_NAME, accountName);
		ContentProviderOperation.Builder builder = newInsertCpo(
				RawContacts.CONTENT_URI, mIsSyncOperation, true).withValues(
				mValues);
		mBatchOperation.add(builder.build());
	}

	public ContactOperations(Context context, long rawContactId,
			boolean isSyncOperation, BatchOperation batchOperation) {
		this(context, isSyncOperation, batchOperation);
		mIsNewContact = false;
		mRawContactId = rawContactId;
	}

	/**
	 * Adds a contact name. We can take either a full name ("Bob Smith") or
	 * separated first-name and last-name ("Bob" and "Smith").
	 * 
	 * @param fullName
	 *            The full name of the contact - typically from an edit form Can
	 *            be null if firstName/lastName are specified.
	 * @param firstName
	 *            The first name of the contact - can be null if fullName is
	 *            specified.
	 * @param lastName
	 *            The last name of the contact - can be null if fullName is
	 *            specified.
	 * @return instance of ContactOperations
	 */
	public ContactOperations addName(String fullName, String firstName,
			String lastName) {
		mValues.clear();

		if (!TextUtils.isEmpty(fullName)) {
			mValues.put(StructuredName.DISPLAY_NAME, fullName);
			mValues.put(StructuredName.MIMETYPE,
					StructuredName.CONTENT_ITEM_TYPE);
		} else {
			if (!TextUtils.isEmpty(firstName)) {
				mValues.put(StructuredName.GIVEN_NAME, firstName);
				mValues.put(StructuredName.MIMETYPE,
						StructuredName.CONTENT_ITEM_TYPE);
			}
			if (!TextUtils.isEmpty(lastName)) {
				mValues.put(StructuredName.FAMILY_NAME, lastName);
				mValues.put(StructuredName.MIMETYPE,
						StructuredName.CONTENT_ITEM_TYPE);
			}
		}
		if (mValues.size() > 0) {
			addInsertOp();
		}
		return this;
	}

	/**
	 * Adds an email
	 * 
	 * @param the
	 *            email address we're adding
	 * @return instance of ContactOperations
	 */
	public ContactOperations addEmail(String email) {
		mValues.clear();
		if (!TextUtils.isEmpty(email)) {
			mValues.put(Email.DATA, email);
			mValues.put(Email.TYPE, Email.TYPE_WORK);
			mValues.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE);
			addInsertOp();
		}
		return this;
	}

	public ContactOperations addAltEmail(String email) {
		mValues.clear();
		if (!TextUtils.isEmpty(email)) {
			mValues.put(Email.DATA, email);
			mValues.put(Email.TYPE, Email.TYPE_OTHER);
			mValues.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE);
			addInsertOp();
		}
		return this;
	}

	public ContactOperations addWebPage(String url) {
		mValues.clear();
		if (!TextUtils.isEmpty(url)) {
			mValues.put(Website.DATA, url);
			mValues.put(Website.TYPE, Website.TYPE_WORK);
			mValues.put(Website.MIMETYPE, Website.CONTENT_ITEM_TYPE);
			addInsertOp();
		}
		return this;
	}

	/**
	 * Adds a phone number
	 * 
	 * @param phone
	 *            new phone number for the contact
	 * @param phoneType
	 *            the type: cell, home, etc.
	 * @return instance of ContactOperations
	 */
	public ContactOperations addPhone(String phone, int phoneType) {
		mValues.clear();
		if (!TextUtils.isEmpty(phone)) {
			mValues.put(Phone.NUMBER, phone);
			mValues.put(Phone.TYPE, phoneType);
			mValues.put(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
			addInsertOp();
		}
		return this;
	}

	/**
	 * Adds a group membership
	 * 
	 * @param id
	 *            The id of the group to assign
	 * @return instance of ContactOperations
	 */
	public ContactOperations addGroupMembership(long groupId) {
		mValues.clear();
		mValues.put(GroupMembership.GROUP_ROW_ID, groupId);
		mValues.put(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
		addInsertOp();
		return this;
	}

	public ContactOperations addAvatar(ContentResolver resolver, String userCode) {
		if (userCode != null) {
			byte[] avatarBuffer = getProfilePic(resolver, userCode);
			if (avatarBuffer != null) {
				mValues.clear();
				mValues.put(Photo.PHOTO, avatarBuffer);
				mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
				addInsertOp();
			}
		}
		return this;
	}

	/**
	 * Adds a profile action
	 * 
	 * @param userId
	 *            the userId of the sample SyncAdapter user object
	 * @param shortName
	 * @return instance of ContactOperations
	 */
	public ContactOperations addProfileAction(String userId, String type,
			String shortName) {
		mValues.clear();
		if (userId != null) {
			mValues.put(ProfileContactColumns.DATA_CODE, userId);
			mValues.put(ProfileContactColumns.DATA_TYPE, type);
			mValues.put(ProfileContactColumns.DATA_NAME, shortName);
			mValues.put(ProfileContactColumns.DATA_SUMMARY,
					mContext.getString(R.string.app_name));
			mValues.put(ProfileContactColumns.DATA_DETAIL,
					mContext.getString(R.string.action_view_profile));
			mValues.put(Data.MIMETYPE, ProfileContactColumns.MIME);
			addInsertOp();
			mValues.clear();
			mValues.put(ProfileContactColumns.DATA_CODE, userId);
			mValues.put(ProfileContactColumns.DATA_TYPE, type);
			mValues.put(ProfileContactColumns.DATA_NAME, shortName);
			mValues.put(ProfileContactColumns.DATA_SUMMARY,
					mContext.getString(R.string.app_name));
			mValues.put(ProfileContactColumns.DATA_DETAIL,
					mContext.getString(R.string.action_view_schedule));
			mValues.put(Data.MIMETYPE, ProfileContactColumns.MIME);
			addInsertOp();

		}
		return this;
	}

	/**
	 * Updates contact's serverId
	 * 
	 * @param serverId
	 *            the serverId for this contact
	 * @param uri
	 *            Uri for the existing raw contact to be updated
	 * @return instance of ContactOperations
	 */
	public ContactOperations updateServerId(long serverId, Uri uri) {
		mValues.clear();
		mValues.put(RawContacts.SOURCE_ID, serverId);
		addUpdateOp(uri);
		return this;
	}

	/**
	 * Updates contact's email
	 * 
	 * @param email
	 *            email id of the sample SyncAdapter user
	 * @param uri
	 *            Uri for the existing raw contact to be updated
	 * @return instance of ContactOperations
	 */
	public ContactOperations updateEmail(String email, String existingEmail,
			int type, Uri uri) {
		if (!TextUtils.equals(existingEmail, email)) {
			mValues.clear();
			mValues.put(Email.DATA, email);
			mValues.put(Email.LABEL, type);
			addUpdateOp(uri);
		}
		return this;
	}

	/**
	 * Updates contact's name. The caller can either provide first-name and
	 * last-name fields or a full-name field.
	 * 
	 * @param uri
	 *            Uri for the existing raw contact to be updated
	 * @param existingFirstName
	 *            the first name stored in provider
	 * @param existingLastName
	 *            the last name stored in provider
	 * @param existingFullName
	 *            the full name stored in provider
	 * @param firstName
	 *            the new first name to store
	 * @param lastName
	 *            the new last name to store
	 * @param fullName
	 *            the new full name to store
	 * @return instance of ContactOperations
	 */
	public ContactOperations updateName(Uri uri, String existingFirstName,
			String existingLastName, String existingFullName, String firstName,
			String lastName, String fullName) {

		mValues.clear();
		if (TextUtils.isEmpty(fullName)) {
			if (!TextUtils.equals(existingFirstName, firstName)) {
				mValues.put(StructuredName.GIVEN_NAME, firstName);
			}
			if (!TextUtils.equals(existingLastName, lastName)) {
				mValues.put(StructuredName.FAMILY_NAME, lastName);
			}
		} else {
			if (!TextUtils.equals(existingFullName, fullName)) {
				mValues.put(StructuredName.DISPLAY_NAME, fullName);
			}
		}
		if (mValues.size() > 0) {
			addUpdateOp(uri);
		}
		return this;
	}

	public ContactOperations updateDirtyFlag(boolean isDirty, Uri uri) {
		int isDirtyValue = isDirty ? 1 : 0;
		mValues.clear();
		mValues.put(RawContacts.DIRTY, isDirtyValue);
		addUpdateOp(uri);
		return this;
	}

	/**
	 * Updates contact's phone
	 * 
	 * @param existingNumber
	 *            phone number stored in contacts provider
	 * @param phone
	 *            new phone number for the contact
	 * @param uri
	 *            Uri for the existing raw contact to be updated
	 * @return instance of ContactOperations
	 */
	public ContactOperations updatePhone(String existingNumber, String phone,
			Uri uri) {
		if (!TextUtils.equals(phone, existingNumber)) {
			mValues.clear();
			mValues.put(Phone.NUMBER, phone);
			addUpdateOp(uri);
		}
		return this;
	}

	public ContactOperations updateAvatar(ContentResolver resolver,
			String userCode, Uri uri) {
		if (userCode != null) {
			byte[] avatarBuffer = getProfilePic(resolver, userCode);
			if (avatarBuffer != null) {
				mValues.clear();
				mValues.put(Photo.PHOTO, avatarBuffer);
				mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
				addUpdateOp(uri);
			}
		}
		return this;
	}

	private byte[] getProfilePic(ContentResolver resolver, String userCode) {
		final Cursor cursor = resolver.query(
				SigarraContract.Profiles.PIC_CONTENT_URI,
				SigarraContract.Profiles.PIC_COLUMNS,
				SigarraContract.Profiles.PROFILE,
				SigarraContract.Profiles.getProfilePicSelectionArgs(userCode),
				null);
		try {
			if (cursor.moveToFirst()) {
				final String path = cursor.getString(cursor
						.getColumnIndex(SigarraContract.Profiles.PIC));
				final Bitmap bitmap = BitmapFactory.decodeFile(path);
				if (bitmap == null) {
					cursor.close();
					return null;
				}
				ByteArrayOutputStream stream = new ByteArrayOutputStream();
				bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
				cursor.close();
				return stream.toByteArray();
			}
		} finally {
			cursor.close();
		}
		return null;
	}

	/**
	 * Updates contact's profile action
	 * 
	 * @param name
	 *            sample SyncAdapter user id
	 * @param uri
	 *            Uri for the existing raw contact to be updated
	 * @return instance of ContactOperations
	 */
	public ContactOperations updateProfileAction(String name, Uri uri) {
		mValues.clear();
		mValues.put(ProfileContactColumns.DATA_NAME, name);
		addUpdateOp(uri);
		return this;
	}

	/**
	 * Adds an insert operation into the batch
	 */
	private void addInsertOp() {

		if (!mIsNewContact) {
			mValues.put(Phone.RAW_CONTACT_ID, mRawContactId);
		}
		ContentProviderOperation.Builder builder = newInsertCpo(
				Data.CONTENT_URI, mIsSyncOperation, mIsYieldAllowed);
		builder.withValues(mValues);
		if (mIsNewContact) {
			builder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
		}
		mIsYieldAllowed = false;
		mBatchOperation.add(builder.build());
	}

	/**
	 * Adds an update operation into the batch
	 */
	private void addUpdateOp(Uri uri) {
		ContentProviderOperation.Builder builder = newUpdateCpo(uri,
				mIsSyncOperation, mIsYieldAllowed).withValues(mValues);
		mIsYieldAllowed = false;
		mBatchOperation.add(builder.build());
	}

	public static ContentProviderOperation.Builder newInsertCpo(Uri uri,
			boolean isSyncOperation, boolean isYieldAllowed) {
		return ContentProviderOperation.newInsert(
				addCallerIsSyncAdapterParameter(uri, isSyncOperation))
				.withYieldAllowed(isYieldAllowed);
	}

	public static ContentProviderOperation.Builder newUpdateCpo(Uri uri,
			boolean isSyncOperation, boolean isYieldAllowed) {
		return ContentProviderOperation.newUpdate(
				addCallerIsSyncAdapterParameter(uri, isSyncOperation))
				.withYieldAllowed(isYieldAllowed);
	}

	public static ContentProviderOperation.Builder newDeleteCpo(Uri uri,
			boolean isSyncOperation, boolean isYieldAllowed) {
		return ContentProviderOperation.newDelete(
				addCallerIsSyncAdapterParameter(uri, isSyncOperation))
				.withYieldAllowed(isYieldAllowed);
	}

	private static Uri addCallerIsSyncAdapterParameter(Uri uri,
			boolean isSyncOperation) {
		if (isSyncOperation) {
			// If we're in the middle of a real sync-adapter operation, then go
			// ahead
			// and tell the Contacts provider that we're the sync adapter. That
			// gives us some special permissions - like the ability to really
			// delete a contact, and the ability to clear the dirty flag.
			//
			// If we're not in the middle of a sync operation (for example, we
			// just
			// locally created/edited a new contact), then we don't want to use
			// the special permissions, and the system will automagically mark
			// the contact as 'dirty' for us!
			return uri
					.buildUpon()
					.appendQueryParameter(
							ContactsContract.CALLER_IS_SYNCADAPTER, "true")
					.build();
		}
		return uri;
	}
}
